<?php

namespace Aspic\View;

use Aspic as A;

/*

	  TIPS
	  ----------

	  - Every child template content must be surround in blocks because that is the top template (the mother template, the layout)
	  that will order the structure of the page.
	  Child have only to redefine some blocks..

	  - If two block have the same name in one template, the second will be considered as a redefinition of the first

	  - If there is text out of a block in a template that extends another, it will be ignored.

	  - Child blocks that are not present in a parent template will be ignore (not in the top template structure)

	  - To stop rendering the view, just use "return null;" when NOT inside a block

	 */

/**
 * @Author Mallory Dessaintes <mdessaintes@gmail.com>
 * @version 1.0
 */
class View implements TemplateLoaderInterface {

    use A\Preset\StdPrintTrait {
        __construct as stdPrintTraitInit;
    }

    const MAIN_BLOCK_NAME = '__ROOT__BLOCK__'; // No user block can have this name

    /**
     * @var string Full path to curent template
     */

    protected $_templatePath;

    /**
     * @var array Template folders where to search for templates
     */
    protected $_templatesDirs;
    protected $_functions;
    protected $_cycle;
    protected $_eachTimes;

    /**
     * An array containing user data:
     * array(
     * 	'css' => array(
     * 		array( // first level of inheritance data (Data of the first template loaded)
     * 			'http://www.abcd.com/css/home.css'
     * 		), array( // second level of inheritance data (Data of the template extends by the first template)
     * 			'http://www.abcd.com/css/layout.css'
     * 		),
     * 		...
     * 	)
     * );
     * This structure is used to make possible including layout data BEFORE current view template (upper temple before child template)
     * For example, if your layout include jquery,  and your template include a jquery plugin,  there will be a
     * javascript error because the include of jquery plugin will preced the including of jquery.
     * With this structure, when calling getRegisteredData(), multiples levels are merged together with as first
     * top level template (so jquery will be include before)
     */
    protected $_userData;
    protected $_parentTemplatesPaths; // List of the parent of the template
    protected $_inheritLevel; // Current level of inheritance
    protected $_topLevel; // Define the maximum level of inheritance that was reached to know when we are no more at the top (difference between it and inheritLevel)
    protected $_blocks; // Associative array : ('blockName'=>'contents')
    protected $_blocksNamesStack;
    protected $_currentBlockOldContents; // When starting at startBlock, we remove old block contents to avoid appending to it new block contents

    public function __construct($view = null, $returnNullIfEmpty = true, $printChar = '_') {
        $this->stdPrintTraitInit(array(), $returnNullIfEmpty, $printChar);

        if ($view) {
            $this->setView($view);
        }

        $this->_templatesDirs = array();
        $this->_functions = array();
        $this->_cycle = array();
        $this->_eachTimes = array();
        $this->_userData = array();

        $this->resetTemplate();
    }

    protected function resetTemplate() {
        $this->_blocksNamesStack = new \SplStack();
        $this->_currentBlockOldContents = array();
        $this->_blocks = array();
        $this->_parentTemplatesPaths = array();
        ;
        $this->_topLevel = 0;
        $this->_inheritLevel = 0;
    }

    public function getVar($var, $ifUnsetReturnValue = null) {
        if (isset($this->_vars[$var]))
            return $this->_vars[$var];
        else
            return $ifUnsetReturnValue;
    }

    /**
     * Now it's the same using    $view->var    then sing    $view('var')
     */
    public function __invoke($var) {
        $this->__get($var);
    }

    public function __call($method, $args) {
        $nameWithoutFirstChar = substr($method, 1);
        $firstChar = substr($method, 0, 1);

        $funcToCall = null;

        if (method_exists($this, $method)) {
            return call_user_func_array(array($this, $method), $args);
        }
        elseif (method_exists($this, $nameWithoutFirstChar) AND $firstChar == $this->_printPrefix) {
            $ret = call_user_func_array(array($this, $nameWithoutFirstChar), $args);
            print $ret;

            return $ret;
        }
        elseif (isset($this->_functions[$method])) {
            return call_user_func_array($this->_functions[$method], $args);
        }
        elseif (isset($this->_functions[$nameWithoutFirstChar])) {
            $ret = call_user_func_array($this->_functions[$method], $args);
            print $ret;

            return $ret;
        }
        else {
            throw new \BadMethodCallException();
        }
    }

    public function appendArray($arr, $value) {
        if (isset($this->_vars[$arr])) {
            if (!is_array($this->_vars[$arr]))
                $this->_vars[$arr] = array();

            $this->_vars[$arr][] = $value;
        }
    }

    public function setView($view) {
        $this->_vars = A\UArray::toArray($view);
    }

    public function addTemplatesDir($path) {
        $this->_templatesDirs[] = $path;
    }

    public function setTemplatesDirs(array $dirs) {
        $this->_templatesDirs = $dirs;
    }

    /**
     * Return the full path to the given template file
     */
    public function getTemplateFullPath($path) {
        $i = 0;

        if (count($this->_templatesDirs)) {
            $fullPath = '';

            while (!is_file($fullPath) AND $i < count($this->_templatesDirs)) {
                $fullPath = $this->_templatesDirs[$i] . '/' . $path;
            }
        }

        if ($i == count($this->_templatesDirs)) {
            if (is_file($path)) {
                $fullPath = $path;
            }
            else {
                throw new A\Exception('Cannot find "' . $path . '" in registered templates directories');
            }
        }

        return $fullPath;
    }

    public function addFunction($name, $func) {
        if (is_callable($func)) {
            $this->_functions[$name] = $func;
        }
        else {
            throw new Exception('Function is not callable (' . $func . ')');
        }
    }

    ####### INHERITANCE FUNCTIONS ####################################

    /*
      STEPS FOR INHERITANCE
      ---------------------

      - Stop at call of inherits function and run the parent template
      - 	Loop at first step until there is parents

      - Then we create a "special root" block that surround the main template
      - Scan top parent for blocks and get their contents (we store it as type contents, when there is no block arround and as "block" type when there is a block in a block)
      - 	Reverse first loop getting blocks from each parent from the top parent and rewrite block each times (sort of cascading style sheet)
      - Print view from the "special root" block
     */

    protected function inherits($parentTemplatePath) {
        $this->_blocks = array(); // This avoid calling startBlock before inherits (Main block will not be earased because it end after all inherits)
        $this->_topLevel++;
        $this->_inheritLevel++;

        $this->_parentTemplatesPaths[] = $parentTemplatePath;

        require $this->getTemplateFullPath($parentTemplatePath);

        if ($this->_isAtRoot()) {
            // We close the main block when the top template ends
            // When there is no inheritance, the root block is ended in the loadTemplate method
            $this->endBlock(self::MAIN_BLOCK_NAME);
        }

        $this->_inheritLevel--;
    }

    /**
     * Return true if we are at the top template
     */
    protected function _isAtRoot() {
        return $this->_topLevel == $this->_inheritLevel;
    }

    protected function startBlock($blockName) {
        $blockContents = ob_get_contents();
        ob_end_clean();

        // That avoid child block to be added at the end of the root block when we are not at the root template
        // Either we are at the root template and then we add a new child in main block or in a child block
        // or we are in a child template and we add a child to an predifined block (defined in the root template)
        if (
            // First test below:
            // There must be a parent block to add a child to it
            // This return false when we are creating the root block
            // and each time we create a block in a child template
            $this->_blocksNamesStack->count() > 0 AND // There must be a parent block to append to it the current block (when starting main block)
            // This avoid writing out of a block when not in the top parent template
            ($this->_isAtRoot() OR
                $this->_blocksNamesStack->top() != self::MAIN_BLOCK_NAME)
        ) {
            $this->_blocks[$this->_blocksNamesStack->top()][] = array('type' => 'contents', 'value' => $blockContents);
            // We add to the current block a new child : the starting block
            $this->_blocks[$this->_blocksNamesStack->top()][] = array('type' => 'block', 'value' => $blockName);
        }
        // else we are in a child template out of any block and this is ignored
        // Then we continue in the starting block

        $this->_blocksNamesStack->push($blockName);
        $this->_currentBlockOldContents = (isset($this->_blocks[$blockName]) ? $this->_blocks[$blockName] : array());
        $this->_blocks[$blockName] = array();

        ob_start();
    }

    protected function endBlock($blockName = '') {
        if ($this->_blocksNamesStack->count() == 0) {
            throw new A\Exception('Cannot end block because no blocks has been opened before');
        }

        $blockName = $this->_blocksNamesStack->pop();

        $blockContents = ob_get_contents();
        ob_end_clean();

        $this->_blocks[$blockName][] = array('type' => 'contents', 'value' => $blockContents);

        ob_start();
    }

    /**
     * Return the parent block content
     */
    protected function getParentBlock() {
        return $this->_returnBlock($this->_currentBlockOldContents);
    }

    /**
     *
     * @param string $filename Template relative path from $this->_templatesDir
     * @param bool $reset When we want to load another template after a first and not from inside the current template,
     * it is needed to refresh blocks
     */
    public function loadTemplate($filename, $reset = false) {
        if ($reset) {
            $this->resetTemplate();
        }

        if ($this->_blocksNamesStack->count() == 0) { // => loadTemplate calls are not nested
            // First, we calculate the blocks until the top parent
            // To avoid writing something that is not in a block in childs templates
            ob_start();

            // The root block start at the beginning
            $this->startBlock(self::MAIN_BLOCK_NAME);

            $templateFullPath = $this->getTemplateFullPath($filename);
            $this->_templatePath = $templateFullPath;

            require $templateFullPath;

            // If there is no parent template, then the root block ends at the end of the template
            // If there is inheritance, then the root block ends when the top tpl ends (see inherits method)
            if (count($this->_parentTemplatesPaths) == 0) {
                $this->endBlock(self::MAIN_BLOCK_NAME);
            }

            ob_end_clean();

            $this->_writeBlock($this->_blocks[self::MAIN_BLOCK_NAME]);

            $this->resetTemplate();
        }
        else { // => loadTemplate calls are nested
            $className = get_called_class($this);
            $nestedView = new $className($this->_vars);
            $nestedView->setTemplatesDirs($this->_templatesDirs);

            $nestedView->loadTemplate($filename);
        }
    }

    /**
     *
     * @param type $filename
     * @return string The template output
     */
    public function getLoadedTemplate($filename) {
        ob_start();

        $this->loadTemplate($filename);

        $data = ob_get_contents();
        ob_end_clean();

        return $data;
    }

    public function getCurrentTemplatePath() {
        return $this->_templatePath;
    }

    protected function _writeBlock($block) { // array('type'=>'contents/block', value=>'contents/blockName')
        foreach ($block as $blockItem) {
            if ($blockItem['type'] == 'block') {
                $this->_writeBlock($this->_blocks[$blockItem['value']]);
            }
            else { // type = contents
                print $blockItem['value'];
            }
        }
    }

    protected function _returnBlock($block) {
        ob_start();

        $this->_writeBlock($block);

        $ct = ob_get_contents();
        ob_end_clean();

        return $ct;
    }

    ####### VIEW FUNCTIONS ####################################

    protected function escape($str) {
        return \Aspic\Security\Security::escapeXss($str);
    }

    protected function escapeJs($str) {
        return \Aspic\Security\Security::escapeJs($str);
    }

    protected function esc($str) {
        return $this->escape($str);
    }

    /**
     * Return alternately v1 and v2
     *
     * Ex:  cycle('blue','red');
     * Ex2: cycle('blue','red','uniqueValue');
     *
     * @param string uniqueId Id of the cycle => just if more than one cycle have same values (to identify the current index)
     */
    protected function cycle($v1, $v2, $uniqueId = null) {
        if ($uniqueId == null)
            $id = $v1 . $v2;
        else
            $id = $v1 . $v2 . $uniqueId;

        if (!isset($this->_cycle[$id]))
            $index = $this->_cycle[$id] = 0;
        else
            $index = ++$this->_cycle[$id];

        return func_get_arg($index % 2);
    }

    /**
     * @deprecated
     */
    protected function eachTimes($value, $times, $else = '', $uniqueId = null) {
        return call_user_func(array($this, 'cycleTimes'), $value, $times, $else, false, $uniqueId);
    }

    /**
     * Return $value every $times calls, returning else in the other case
     *
     * @param bool $startWithValue Define if the first call return the value or not
     */
    protected function cycleTimes($value, $times, $else = '', $startWithValue = false, $uniqueId = null) {
        if ($uniqueId == null)
            $id = $value . $else . $startWithValue;
        else
            $id = $value . $else . $startWithValue . $uniqueId;

        if (!isset($this->_eachTimes[$id]))
            $index = $this->_eachTimes[$id] = ($startWithValue) ? 0 : 1;
        else
            $index = ++$this->_eachTimes[$id];

        if ($index % $times == 0)
            return $value;
        else
            return $else;
    }

    /**
     * Return var if it's not empty or varReplacer in the other case
     */
    protected function ifEmpty($var, $varReplacer) {
        return A\UString::ifEmpty($var, $varReplacer);
    }

    /**
     * This function is a helper for a "if else" conditions in a string concatenation
     * If v1 == v2 return egalValue else return notEgalValue
     */
    protected function ifEqualReturn($v1, $v2, $equalValue, $notEqualValue = '') {
        if ($v1 == $v2)
            return $equalValue;
        else
            return $notEqualValue;
    }

    /**
     * Format a string
     *
     * @param string $var Variable to format
     * @param string $format formats list separate by $sep like 'htmlspecialchars-nl2br'
     */
    protected function format($var, $format, $sep = '-') {
        $arrFormat = explode($sep, $format);
        $arrFormat = array_map('strtolower', $arrFormat);

        $fVar = $var;

        foreach ($arrFormat as $f) {
            if ($f == 'nl2br')
                $fVar = nl2br($fVar);

            elseif ($f == 'htmlspecialchars') {
                $fVar = htmlspecialchars($fVar);
            }
            elseif ($f == 'htmlentities') {
                $fVar = htmlentities($fVar);
            }
            else {
                if (function_exists($f)) {
                    $fVar = call_user_func($f, $fVar);
                }
            }
        }

        return $fVar;
    }

    /**
     * Generate html code for a tag with attributes and contents
     */
    protected function tag($name, $attrs = array(), $content = null) {
        $html = '<' . $name . ' ';
        $attrsStr = array();

        foreach ($attrs as $n => $v) {
            $attrsStr[] = $n . '="' . htmlentities($v) . '"';
        }

        $html .= implode(' ', $attrsStr);

        if ($content) {
            $html .= '>' . $content . '</' . $name . '>';
        }
        else {
            $html .= ' />';
        }

        return $html;
    }

    /**
     * Generate the html code for a css inclusion
     * @param string|array A CSS url to write header or an array of CSS urls.
     */
    protected function cssInclude($cssData) {
        if (!is_array($cssData)) {
            return '<link rel="stylesheet" type="text/css" href="' . $cssData . '" />';
        }
        else {
            $res = '';

            foreach ($cssData as $fileName) {
                $res .= $this->cssInclude($fileName) . "\n";
            }

            return $res;
        }
    }

    /**
     * Generate the html code for a javascript inclusion
     */
    protected function jsInclude($jsData) {
        if (!is_array($jsData)) {
            return '<script type="text/javascript" src="' . $jsData . '"></script>';
        }
        else {
            $res = '';

            foreach ($jsData as $fileName) {
                $res .= $this->jsInclude($fileName) . "\n";
            }

            return $res;
        }
    }

    /**
     * Usefull to register CSS, JS files that the layout will print in the header
     * ex: registerData('css', 'path/to/css/file');
     * then the layout use getRegisteredData('css') to recover css files to include
     */
    protected function registerData($type, $data, $atTop = false) {
        if ($atTop) {
            $this->_userData[$type][$this->_inheritLevel] = A\UArray::insertAtIndex($this->_userData[$type][$this->_inheritLevel], $data, 0);
        }
        else {
            $this->_userData[$type][$this->_inheritLevel][] = $data;
        }
    }

    protected function getRegisteredData($type) {
        $dataStructure = A\Util::getIfSet($this->_userData[$type]);

        if ($dataStructure) {

            // Now we merge arrays of differents levels (template inheritance) together
            // First we reverse array to sort DESC by _inheritsLevel
            $dataStructure = array_reverse($dataStructure);
            $merge = array();

            foreach ($dataStructure as $data) {
                $merge = array_merge($merge, $data);
            }

            return $merge;
        }
        else {
            return null;
        }
    }

}